/*
 * Copyright (c) 2010, Tangedyn <tangedyn at gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   * Neither the name of the Mew Developers nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

package yawning.mew.cat;

import java.util.HashMap;

import yawning.mew.ProcEffect;
import yawning.mew.PropertiesBase.IProperty;
import yawning.mew.Report;
import yawning.mew.character.Druid.Talent;
import yawning.mew.sim.SimulationEngine;
import yawning.mew.sim.Simulation;
import yawning.mew.sim.SimulationProc;
import yawning.mew.sim.Strategy.IAction;
import yawning.mew.sim.Strategy.IStatus;

public class CatModelSimulation implements Simulation {
	private static final String DEFAULT_STRATEGY_FILE = "yawning/mew/cat/CatModelSimulator.script";
	private static final String VERSION_INFO = "x.x.x";

	// Actions that the user can specify.
	public enum Action implements IAction {
		RAVAGE,
		MANGLE,
		RAKE,
		SHRED,
		RIP,
		BITE,
		ROAR,
		FERALFF,
		BERSERK,

		FERALCHARGE,
		TIGERSFURY,
		SKULLBASH,
		POTION,

		// External damage boosting buffs are manually triggered here.
		HEROISM,
		UNHOLYFRENZY,
		SHATTERINGTHROW;
	}

	public enum SimulatorProperty implements IProperty {
		ASSUMEEXTERNALBLEED("Simulator.AssumeExternalBleed", "false"),		// Mob is always bleeding for purposes of Rend and Tear.
		ASSUMEEXTERNALFF("Simulator.AssumeExternalFF", "false"),			// Mob is always fully debuffed with the MajorArmor debuff.
		ASSUMEEXTERNALMANGLE("Simulator.AssumeExternalMangle", "false");	// Mob is always fully debuffed with the Bleed Damage debuff.

		private final String propertyKey;
		private final String defaultValue;

		private SimulatorProperty(String k) {
			this(k, null);
		}

		private SimulatorProperty(String k, String v) {
			propertyKey = k;
			defaultValue = v;
		}

		public String getKey() {
			return this.propertyKey;
		}

		public String getDefault() {
			return this.defaultValue;
		}
	}

	@Override
	public Class<?>[] getExportClasses() {
		return new Class<?>[] {
				Action.class,
				CatModelSimState.class
		};
	}

	@Override
	public String getVersionInfo() {
		return VERSION_INFO;
	}

	@Override
	public IStatus initSimulation(SimulationEngine su) {
		CatModelSimState state = new CatModelSimState(su);
		if (!su.hasStrategy()) su.setStrategy(Thread.currentThread().getContextClassLoader().getResourceAsStream(DEFAULT_STRATEGY_FILE));
		return state;
	}

	@Override
	public Report getReport(IStatus status) {
		if (!(status instanceof CatModelSimState)) return null;
		CatModelSimState simStats = (CatModelSimState) status;

		CatReport report = new CatReport(); // This is somewhat expensive. :(

		// Derive some things.
		double pFail = simStats.mModel.getMissChance() + simStats.mModel.getDodgeChance();

		int nrWhiteAttacks = simStats.nrWhiteMiss + simStats.nrWhiteDodge + simStats.nrWhiteGlance + simStats.nrWhiteCrit + simStats.nrWhiteHit;
		int nrMangleAttacks = simStats.nrMangleMiss + simStats.nrMangleDodge + simStats.nrMangleCrit + simStats.nrMangleHit;
		int nrShredAttacks = simStats.nrShredMiss + simStats.nrShredDodge + simStats.nrShredCrit + simStats.nrShredHit;
		int nrRakeAttacks = simStats.nrRakeMiss + simStats.nrRakeDodge + simStats.nrRakeCrit + simStats.nrRakeHit;
		int nrRipAttacks = simStats.nrRipMiss + simStats.nrRipDodge + simStats.nrRipHit;
		int nrBiteAttacks = simStats.nrBiteMiss + simStats.nrBiteDodge + simStats.nrBiteCrit + simStats.nrBiteHit;
		int nrRavageAttacks = simStats.nrRavageMiss + simStats.nrRavageDodge + simStats.nrRavageCrit + simStats.nrRavageHit;
		int nrRakeTicks = simStats.nrRakeDotCrit + simStats.nrRakeDotHit;
		int nrRipTicks = simStats.nrRipDotCrit + simStats.nrRipDotHit;

		double pMangleFail = (double) (simStats.nrMangleMiss + simStats.nrMangleDodge) / nrMangleAttacks;
		double pShredFail = (double) (simStats.nrShredMiss + simStats.nrShredDodge) / nrShredAttacks;
		double pRakeFail = (double) (simStats.nrRakeMiss + simStats.nrRakeDodge) / nrRakeAttacks;
		double pRipFail = (double) (simStats.nrRipMiss + simStats.nrRipDodge) / nrRipAttacks;
		double pBiteFail = (double) (simStats.nrBiteMiss + simStats.nrBiteDodge) / nrBiteAttacks;
		double pRavageFail = (double) (simStats.nrRavageMiss) / nrRavageAttacks;

		long ripDamageDpe;
		int nrRipAttacksDpe;
		double pRipFailDpe;
		if (simStats.mModel.druid.getTalent(Talent.BLOODINTHEWATER) > 0 && simStats.mModel.getBloodInTheWaterTime() > 0) {
			ripDamageDpe = simStats.ripDamageBase;
			nrRipAttacksDpe = simStats.nrRipBase + simStats.nrRipFailBase;
			pRipFailDpe = (double) simStats.nrRipFailBase / nrRipAttacksDpe;
		} else {
			ripDamageDpe = simStats.ripDamage;
			nrRipAttacksDpe = nrRipAttacks;
			pRipFailDpe = pRipFail;
		}

		long totalDamage = simStats.whiteDamage + simStats.furySwipesDamage + simStats.mangleDamage + simStats.rakeDamage + simStats.rakeDotDamage + simStats.shredDamage + simStats.ripDamage + simStats.biteDamage + simStats.ravageDamage + simStats.procPhysDamage + simStats.procSpellDamage;
		double bitwTime = simStats.mSimUtil.getEndTime() * simStats.mModel.getBloodInTheWaterTime();

		// Figure out energy regeneration rate from energy expended.  (Adjust for starting with 100.)
		double energySpent = 0;
		energySpent += simStats.nrFeralCharge * simStats.mModel.feralChargeCost;
		energySpent += simStats.nrSR * simStats.mModel.srCost;
		energySpent += (nrMangleAttacks > 0) ? nrMangleAttacks * simStats.mModel.mangleCost * ((1 - pMangleFail) + 0.2 * pMangleFail) : 0;
		energySpent += (nrShredAttacks > 0) ? nrShredAttacks * simStats.mModel.shredCost * ((1 - pShredFail) + 0.2 * pShredFail) : 0;
		energySpent += (nrRakeAttacks > 0) ? nrRakeAttacks * simStats.mModel.rakeCost * ((1 - pRakeFail) + 0.2 * pRakeFail) : 0;
		energySpent += (nrRipAttacks > 0) ? nrRipAttacks * simStats.mModel.ripCost * ((1 - pRipFail) + 0.2 * pRipFail) : 0;
		energySpent += (nrBiteAttacks > 0) ? nrBiteAttacks * simStats.mModel.biteCost * ((1 - pBiteFail) + 0.2 * pBiteFail) : 0;
		energySpent += (nrRavageAttacks > 0) ? nrRavageAttacks * simStats.mModel.ravageCost * ((1 - pRavageFail) + 0.2 * pRavageFail) : 0;
		double energyRegenRate = (energySpent - (simStats.mModel.baseEnergyCap - simStats.energyTickEv.mEnergy)) / simStats.mSimUtil.getEndTime();

		//report.mDps = cycleDPS.dps;						// DPS
		if (simStats.mModel.druid.getTalent(Talent.BLOODINTHEWATER) > 0 && simStats.mModel.getBloodInTheWaterTime() > 0) {
    		report.mDpsBase = (double) simStats.totalDamageBase / (simStats.mSimUtil.getEndTime() - bitwTime);		// DPS > 25%
    		report.mDpsBitW = (double) (totalDamage - simStats.totalDamageBase) / bitwTime;	// DPS < 25%
    	} else {
    		report.mDpsBase = 0;
    		report.mDpsBitW = 0;
    	}

		// FIXME: pFail will not react well to encounters ending with hit/expertise procs up.
		// FIXME: Approximating average yellow crit rate from DoTs is far from ideal.
		report.pFail = pFail;												// pMiss + Dodge
		report.pWhiteCrit = (double) simStats.nrWhiteCrit / nrWhiteAttacks;	// White crit rate
		report.pYellowCrit = (double) (simStats.nrRakeDotCrit + simStats.nrRipDotCrit) / (nrRakeTicks + nrRipTicks);	// Yellow crit rate (Base, Bite handled elsewhere.)
		report.pWhiteCritCap = simStats.mModel.getWhiteCritCap();	// White crit cap
		report.mSwingTimer = simStats.mSimUtil.getEndTime() / nrWhiteAttacks;						// Swing timer
		report.mOoCPPM = (double) simStats.nrOoCProcs / (simStats.mSimUtil.getEndTime() / 60.0);	// Omen of Clarity PPM
		report.mOoCReturn = (double) simStats.oocEnergyReturn / simStats.nrOoCProcs;	// Energy/OoC Proc
		report.mEnergyRegenRate = energyRegenRate;							// Energy regeneration rate

		report.mNrSavageRoar = simStats.nrSR;								// Number of Savage Roars
		report.mSavageRoarUptime = simStats.savageRoarEv.getUpTime() / simStats.mSimUtil.getEndTime();	// Savage Roar Uptime
		if (simStats.nrSR > 0)
			report.mSavageRoarCPs = simStats.nrSrCPs / simStats.nrSR;		// Savage Roar CP/cast
		else
			report.mSavageRoarCPs = 0;

		report.mNrMangles = simStats.nrMangleCrit + simStats.nrMangleHit;	// Number of Mangles
		report.mMangleUptime = simStats.mangleEv.getUpTime() / simStats.mSimUtil.getEndTime();			// Mangle debuff uptime

		report.mNrShreds = simStats.nrShredCrit + simStats.nrShredHit;		// Number of Shreds

		report.mNrRakes = simStats.nrRakeCrit + simStats.nrRakeHit;			// Number of Rakes
		report.mRakeUptime = simStats.rakeEv.getUpTime() / simStats.mSimUtil.getEndTime();				// Rake DoT uptime

		report.mNrRips = simStats.nrRipHit;									// Number of Rips
		report.mRipUptime = simStats.ripEv.getUpTime() / simStats.mSimUtil.getEndTime();				// Rip DoT uptime

		report.mNrBites = simStats.nrBiteCrit + simStats.nrBiteHit;			// Number of Bites
		if (simStats.mModel.druid.getTalent(Talent.BLOODINTHEWATER) > 0 && simStats.mModel.getBloodInTheWaterTime() > 0) {
			report.mNrBitesBase = simStats.nrBiteBase;						// Number of Bites > 25%
			report.mNrBitesPerRip = (double) simStats.nrBiteBase / simStats.nrRipBase;	// Number of Bites per Rip (> 25%)
			report.mNrBitesBitW = simStats.nrBiteCrit + simStats.nrBiteHit - simStats.nrBiteBase;	// Number of Bites < 25%
		} else {
			report.mNrBitesBase = report.mNrBites;							// Number of Bites > 25%
			report.mNrBitesPerRip = report.mNrBites / report.mNrRips;		// Number of Bites per Rip (> 25%)
			report.mNrBitesBitW = 0;										// Number of Bites < 25%
		}
		report.pBiteCrit = (double) simStats.nrBiteCrit / nrBiteAttacks;	// Bite crit

		report.mNrTigersFurys = simStats.nrTF;
		report.mTigersFuryUptime = simStats.tigersFuryEv.getUpTime() / simStats.mSimUtil.getEndTime();
		report.mNrBerserks = simStats.nrBerserk;
		report.mBerserkUptime = simStats.berserkEv.getUpTime() / simStats.mSimUtil.getEndTime();

		report.mSegmentTime = simStats.mSimUtil.getEndTime();										// Segment time (Encounter duration for sim.)

		// FIXME: This doesn't properly account for having to ramp up Feral FF if the Druid is speced like a tard.
		// FIXME: This kind of sucks.
		double feralFFUptime = simStats.feralFFEv.getUpTime() / simStats.mSimUtil.getEndTime();
		double shatteringThrowUptime = simStats.shatteringThrowEv.getUpTime() / simStats.mSimUtil.getEndTime();
		simStats.mModel.calculateTargetArmor(0, false);
		double armorValueBase = simStats.mModel.targetArmor;
		double armorMitBase = simStats.mModel.multTargetArmor;
		simStats.mModel.calculateTargetArmor(0, true);
		double armorValueBaseS = simStats.mModel.targetArmor;
		double armorMitBaseS = simStats.mModel.multTargetArmor;
		simStats.mModel.calculateTargetArmor(3, false);
		double armorValueDebuffed = simStats.mModel.targetArmor;
		double armorMitDebuffed = simStats.mModel.multTargetArmor;
		simStats.mModel.calculateTargetArmor(3, true);
		double armorValueDebuffedS = simStats.mModel.targetArmor;
		double armorMitDebuffedS = simStats.mModel.multTargetArmor;
		simStats.mModel.calculateTargetArmor(simStats.feralFFEv.mStacks, false);	// Put things back to how they were, redundant but side effects are lame.

		armorValueBase = armorValueBase * (1 - shatteringThrowUptime) + armorValueBaseS * shatteringThrowUptime;
		armorMitBase = armorMitBase * (1 - shatteringThrowUptime) + armorMitBaseS * shatteringThrowUptime;
		armorValueDebuffed = armorValueDebuffed * (1 - shatteringThrowUptime) + armorValueDebuffedS * shatteringThrowUptime;
		armorMitDebuffed = armorMitDebuffed * (1 - shatteringThrowUptime) + armorMitDebuffedS * shatteringThrowUptime;

		report.mArmorValue = armorValueBase * (1 - feralFFUptime) + armorValueDebuffed * feralFFUptime;		// Armor value
		report.mArmorMitigation = armorMitBase * (1 - feralFFUptime) + armorMitDebuffed * feralFFUptime;	// Armor mitigation (%)

		report.pWhiteDamage = (double) simStats.whiteDamage / totalDamage;				// White Damage (%)
		report.pFurySwipesDamage = (double) simStats.furySwipesDamage / totalDamage;	// Fury Swipes Damage (%)
		report.pRavageDamage = (double) simStats.ravageDamage / totalDamage;			// Ravage Damage (%)
		report.pMangleDamage = (double) simStats.mangleDamage / totalDamage;			// Mangle Damage (%)
		report.pShredDamage = (double) simStats.shredDamage / totalDamage;				// Shred Damage (%)
		report.pRakeDamage = (double) (simStats.rakeDamage + simStats.rakeDotDamage) / totalDamage;	// Rake Damage (%)
		report.pRipDamage = (double) simStats.ripDamage / totalDamage;					// Rip Damage (%)
		report.pBiteDamage = (double) simStats.biteDamage / totalDamage;				// Bite Damage (%)
		report.pOtherDamage = (double) (simStats.procPhysDamage + simStats.procSpellDamage) / totalDamage;	// Other Damage (%)

		// FIXME: Rip DPE is slightly lower than what it actually is due to how I snapshot stats when Feast of Flesh kicks in.
		report.mAvgWhiteAttack = (double) simStats.whiteDamage / (simStats.mSimUtil.getEndTime() / report.mSwingTimer);	// Average white attack.
		if (nrRavageAttacks > 0) report.mAvgRavageAttack = (double) simStats.ravageDamage / nrRavageAttacks; 			// Average Ravage attack.
		if (nrMangleAttacks > 0) report.mMangleDPE = (double) simStats.mangleDamage / (nrMangleAttacks * simStats.mModel.mangleCost * ((1 - pMangleFail) + 0.2 * pMangleFail));	// Mangle damage/energy
		if (nrShredAttacks > 0) report.mShredDPE = (double) simStats.shredDamage / (nrShredAttacks * simStats.mModel.shredCost * ((1 - pShredFail) + 0.2 * pShredFail));		// Shred damage/energy
		if (nrRakeAttacks > 0) report.mRakeDPE = (double) (simStats.rakeDamage + simStats.rakeDotDamage) / (nrRakeAttacks * simStats.mModel.rakeCost * ((1 - pRakeFail) + 0.2 * pRakeFail));	// Rake damage/energy
		if (nrRipAttacks > 0) report.mRipDPE = (double) ripDamageDpe / (nrRipAttacksDpe * simStats.mModel.ripCost * ((1 - pRipFailDpe) + 0.2 * pRipFailDpe));				// Rip damage/energy
		if (nrBiteAttacks > 0) report.mBiteDPE = (double) simStats.biteDamage / (nrBiteAttacks * simStats.mModel.biteCost * ((1 - pBiteFail) + 0.2 * pBiteFail));				// Bite damage/energy

		report.mProcUptimes = new HashMap<ProcEffect, Double>();
		for (SimulationProc proc : simStats.mSimUtil.getProcs()) {
			report.mProcUptimes.put(proc.getProcEffect(), proc.getUpTime() / simStats.mSimUtil.getEndTime());
		}

		// Calculate DPS
		totalDamage = simStats.whiteDamage + simStats.furySwipesDamage + simStats.mangleDamage + simStats.rakeDamage + simStats.rakeDotDamage + simStats.shredDamage + simStats.ripDamage + simStats.biteDamage + simStats.ravageDamage + simStats.procPhysDamage + simStats.procSpellDamage;
		double dps = totalDamage / simStats.mSimUtil.getEndTime();
		report.setPerformance(dps);

		return report;
	}
}
